/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.camel.spring.boot.util;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.camel.CamelContext;
import org.apache.camel.Component;
import org.apache.camel.PropertyBindingException;
import org.apache.camel.spi.BeanIntrospection;
import org.apache.camel.spi.PropertyConfigurer;
import org.apache.camel.support.PluginHelper;
import org.apache.camel.support.PropertyBindingSupport;
import org.apache.camel.support.service.ServiceHelper;
import org.apache.camel.util.ObjectHelper;

/**
 * To help configuring Camel properties that have been defined in Spring Boot configuration files.
 */
public final class CamelPropertiesHelper {

    private CamelPropertiesHelper() {
    }

    @SuppressWarnings({ "unchecked" })
    public static void copyProperties(CamelContext camelContext, Object source, Object target) {
        ObjectHelper.notNull(camelContext, "camel context");
        ObjectHelper.notNull(source, "source");
        ObjectHelper.notNull(target, "target");

        CamelPropertiesHelper.setCamelProperties(camelContext, target,
                source instanceof Map ? (Map) source : getNonNullProperties(camelContext, source), false);
    }

    /**
     * Sets the properties on the target bean.
     * <p/>
     * This method uses {@link PropertyBindingSupport} and therefore offers its capabilities such as:
     * <ul>
     * <li>property placeholders - Keys and values using Camels property placeholder will be resolved</li>
     * <li>nested - Properties can be nested using the dot syntax (OGNL and builder pattern using with as prefix), eg
     * foo.bar=123</li>
     * <li>map</li> - Properties can lookup in Map's using map syntax, eg foo[bar] where foo is the name of the property
     * that is a Map instance, and bar is the name of the key.</li>
     * <li>list</li> - Properties can refer or add to in List's using list syntax, eg foo[0] where foo is the name of
     * the property that is a List instance, and 0 is the index. To refer to the last element, then use last as
     * key.</li>
     * </ul>
     * This implementation sets the properties using the following algorithm in the given order:
     * <ul>
     * <li>reference by bean id - Values can refer to other beans in the registry by prefixing with with # or #bean: eg
     * #myBean or #bean:myBean</li>
     * <li>reference by type - Values can refer to singleton beans by their type in the registry by prefixing with
     * #type: syntax, eg #type:com.foo.MyClassType</li>
     * <li>autowire by type - Values can refer to singleton beans by auto wiring by setting the value to #autowired</li>
     * <li>reference new class - Values can refer to creating new beans by their class name by prefixing with #class, eg
     * #class:com.foo.MyClassType</li>
     * <li>value as lookup - The value is used as-is (eg like #value) to lookup in the Registry if there is a bean then
     * its set on the target</li>
     * </ul>
     * When an option has been set on the target bean, then its removed from the given properties map. If all the
     * options has been set, then the map will be empty. The implementation ignores case for the property keys.
     *
     * @param context
     *            the CamelContext
     * @param target
     *            the target bean
     * @param properties
     *            the properties
     * @param failIfNotSet
     *            whether to fail if an option either does not exists on the target bean or if the option cannot be due
     *            no suitable setter methods with the given type
     *
     * @return <tt>true</tt> if at least one option was configured
     *
     * @throws IllegalArgumentException
     *             is thrown if an option cannot be configured on the bean because there is no suitable setter method
     *             and failOnNoSet is true.
     */
    public static boolean setCamelProperties(CamelContext context, Object target, Map<String, Object> properties,
            boolean failIfNotSet) {
        ObjectHelper.notNull(context, "context");
        ObjectHelper.notNull(target, "target");
        ObjectHelper.notNull(properties, "properties");
        boolean rc = false;

        PropertyConfigurer configurer = null;
        if (target instanceof Component) {
            // the component needs to be initialized to have the configurer ready
            ServiceHelper.initService(target);
            configurer = ((Component) target).getComponentPropertyConfigurer();
        }

        Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Object> entry = it.next();
            String name = entry.getKey();
            Object value = entry.getValue();
            String stringValue = value != null ? value.toString() : null;
            boolean hit = false;
            try {
                hit = PropertyBindingSupport.build().withConfigurer(configurer).withIgnoreCase(true).bind(context,
                        target, name, value);
            } catch (PropertyBindingException e) {
                // no we could not and this would be thrown if we attempted to set a value on a property which we cannot
                // do type conversion as
                // then maybe the value refers to a spring bean in the registry so try this
                if (stringValue != null) {
                    if (stringValue.startsWith("#")) {
                        stringValue = stringValue.substring(1);
                    }
                    // use #bean: to lookup
                    stringValue = "#bean:" + stringValue;
                    hit = PropertyBindingSupport.build().withIgnoreCase(true).bind(context, target, name, stringValue);
                }
            }

            if (hit) {
                // must remove as its a valid option and we could configure it
                it.remove();
                rc = true;
            } else if (failIfNotSet) {
                throw new IllegalArgumentException("Cannot configure option [" + name + "] with value [" + stringValue
                        + "] as the bean class [" + ObjectHelper.classCanonicalName(target)
                        + "] has no suitable setter method, or not possible to lookup a bean with the id ["
                        + stringValue + "] in Spring Boot registry");
            }
        }

        return rc;
    }

    /**
     * Gets all the non-null properties from the given object,
     *
     * @param camelContext
     *            the camel context
     * @param target
     *            the object
     *
     * @return the properties (non-null only)
     */
    public static Map<String, Object> getNonNullProperties(CamelContext camelContext, Object target) {
        Map<String, Object> properties = new HashMap<>();

        BeanIntrospection bi = PluginHelper.getBeanIntrospection(camelContext);
        bi.getProperties(target, properties, null, false);

        return properties;
    }

}
